3. 受限直接执行

本节将阐述进程运行的机制。当我们构建这样的机制时,需要面临一个关键问题:

关键问题:如何高效、可控地虚拟化 CPU?

这个问题的缘由是显然的,高效意味着这套机制不应增加过多的额外开销;可控是因为操作系统负责所有的物理资源管理。如果没有控制权,应用进程就可以永久接管机器或越权访问信息。

基本思路

这个问题的答案是 受限直接执行(Limited Direct Execution,LDE) 机制。其可以被分成两部分:“直接执行”,顾名思义,就是直接在 CPU 上运行程序;“受限”,就是指程序直接在 CPU 上运行时仍然确保操作系统的控制权,限制程序可以执行的操作。接下来我们详细解释一下该机制需要解决哪些问题:

受限操作

关键问题:如何保证一个程序在不能完全控制系统的情况下执行某些受限制的操作,例如 I/O 操作?

解决方式是引入 处理器模式 的概念。处理器可以工作在两种模式下,应用程序运行在 用户模式 下,其可执行的功能受到限制,例如不能执行 I/O 操作。而操作系统(内核)运行在 内核模式 下,该模式下可以执行任何操作,包括各种特权操作。

当应用程序需要执行某些在用户模式下不被允许的特权操作时,可以向操作系统发出 系统调用。要执行系统调用,程序需要执行 陷阱(Trap) 指令陷入操作系统内核,并将特权级别提升到内核模式,由操作系统本身代为执行对应的特权行为。完成该行为后,操作系统调用一个特殊的 从陷阱返回 的指令,返回到应用程序中,将结果返回,并将特权级别降低到用户模式。

执行陷阱时,硬件需要小心储存调用方的寄存器,以便操作系统发出从陷阱返回指令时能够正常返回应用程序。例如在 x86 中,处理器会将调用方的诸如 PC、FLAGS 与其它寄存器推送到每个进程的 内核栈(Kernel Stack) 上。从陷阱返回时从栈弹出这些值,并继续执行用户程序。

从应用程序的角度来说,系统调用和普通的过程调用没有什么区别,这是因为系统调用确实 是过程调用,只是系统调用内部 嵌入了陷阱指令。这些指令是使用汇编指令手工编码的,通过使用与内核一致的调用约定设置参数与系统调用号,执行陷阱指令后就可以在约定的位置获得返回值。

从操作系统的角度来说,内核知道程序陷入陷阱后应当执行什么操作,是通过在机器启动时设置 陷阱表(Trap Table) 实现的。机器启动时处于内核模式,操作系统配置陷阱表,通过特殊的指令告诉硬件在发生某些事件时跳转到对应的陷阱处理程序的位置。

有了这些机制,我们就可以回答本小节的关键问题并概括“受限执行”的行为了,如下表所示:

Pasted image 20250531203922.png

进程切换

上面的机制很好,但忽略了一个问题:当应用程序在 CPU 上运行时,这就代表操作系统 没有 在 CPU 上运行,那么当有异常事件发生时,操作系统就不能相应地做出行动。此时只能重启计算机。

重启并非一件坏事

重新启动很有用,因为它让软硬件回归已知、经过大量测试的状态。重启可以回收一些不重启就很难处理的泄漏资源。重启也很容易自动化。

关键问题:操作系统如何重获 CPU 的控制权?

上面的描述已经暗含了一种解决方法:等待系统调用。这被称作 协作 方式,是过去某些操作系统采用的方式。具体而言,操作系统会等待系统调用或非法操作发生,此时 CPU 的控制权就会自然而然转移给操作系统,操作系统就可以进行对应的操作。正常处理合法的系统调用,或终止试图进行非法操作的进程。

然而这个简单的结局方案天真的以为所有的应用进程会合理运行并最终进行系统调用,但这是不现实的。一个简单的死循环程序就可以使得这样的系统宕机,因为进程没有进行任何系统调用,操作系统永远无法重获控制权。因此,我们需要引入 时钟中断(Timer Interrupt),即设置一个时钟每隔几 ms 产生一次中断,中断产生时正在运行的程序立刻暂停,操作系统预先配置的 中断处理程序(Interrupt Handler) 运行,将 CPU 的控制权交给操作系统。同样的,时钟中断与中断处理程序都需要在机器启动时配置,这些也属于特权操作。

当操作系统重新获得控制权时,其需要决定是否继续执行当前程序还是切换另一个程序运行。这个决定由 调度程序 依据调度策略做出,见4. 进程调度策略。如果决定切换另一个进程执行,操作系统就需要执行一些代码保存当前程序的 上下文,包括通用寄存器、PC、内核栈指针等内容,然后恢复另一个程序的对应内容。这被称作 上下文切换。当从陷阱中恢复时,看起来就好像是另一个进程从陷阱中返回并继续执行。在这一整个过程中,发生了两种寄存器的保存与恢复。当时钟中断触发时,硬件将当前运行程序的寄存器存放到进程的内核栈中;当操作系统决定执行上下文切换时,原进程的寄存器从内核栈被操作系统保存到该程序的 进程结构 中。

至此我们得到了一个更完备的机制,如下表所示:

Pasted image 20250531220833.png

而为了实现上述机制,操作系统与硬件都需要支持更多的机制。总结一下,硬件需要提供:

而操作系统需要实现如下功能:

此外,一个中断可能在处理另一个中断时发生,而这种问题的处理方式将在并发部分论述。

在当下的软硬件环境下一次上下文切换所需的时间通常在 0.1 μs ~ 10 μs。一些性能工具(例如)可以准确衡量这些操作需要的时间。此外,许多操作系统操作都是内存密集型的,而内存的读写性能并没有像 CPU 那样在最近几十年间得到了显著提升。因此并非所有操作系统都会跟踪 CPU 的性能。